/*
 * Class
 * RCallerInstance
 */
package pl.abstractvoid.rconnector;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.ImageIcon;
import pl.abstractvoid.datamodel.parameters.ParameterSet;
import pl.abstractvoid.datamodel.parameters.ParameterTable;
import pl.abstractvoid.datamodel.parameters.SingleParameter;
import pl.abstractvoid.datamodel.parameters.exceptions.NoSuchParameterException;
import pl.abstractvoid.languages.LanguageProvider;
import pl.abstractvoid.settings.SettingsProvider;
import rcaller.RCaller;
import rcaller.RCode;
import rcaller.exception.ParseException;

/**
 * Represents the R instance for particular instance of probability distribution
 * which is used to store the data and call R functions.
 * <p>
 * This implementation is based on RCaller library by Mehmet Hakan Satman.
 *
 * @author Wojciech Szałapski
 */
public class RCallerInstance extends RInterpreter implements Serializable {

    /**
     * Directory for temporary files.
     */
    private final static File tempDirectory = new File("TemporaryFiles");

    static {
        if (!tempDirectory.exists()) {
            tempDirectory.mkdir();
        }
    }

    /**
     * Instance of the R caller.
     */
    private final RCaller caller = new RCaller();

    /**
     * R code buffer.
     */
    private final RCode code = new RCode();

    /**
     * Code buffer for building R function calls.
     */
    private StringBuilder codeBuffer;

    /**
     * Constructor initializing R caller and code buffer.
     */
    public RCallerInstance() {
        caller.setRExecutable(SettingsProvider.getInstance().getProperty("RExecutableFile"));
        caller.setFailurePolicy(RCaller.FailurePolicy.CONTINUE);
        caller.cleanRCode();
        code.clear();
        code.addRCode("if (require(\"Runiversal\") == FALSE) {");
        if (System.getProperty("os.name").startsWith("Windows")) {
            code.addRCode("install.packages(\"Runiversal\", pkgs = \"Runiversal_1.0.2.zip\", lib = \"./\", repos = NULL, contriburl = NULL)");
        } else {
            code.addRCode("install.packages(\"Runiversal\", lib = \"./\", repos = \"http://cran.rstudio.com/\")"); 
        }
        code.addRCode("}");
        code.addRCode("library(\"Runiversal\", lib.loc = \"./\")");
        code.addRCode(inputRList + " = c()");
        code.addRCode(plotRList + " = list(generatedPlots = c())");
        code.addRCode("options(error = utils::recover)");
        code.addRCode("options(echo = FALSE)");
        code.addRCode("options(help_type = \"unavailable\")");
        caller.setRCode(code);
        caller.runAndReturnResultOnline(inputRList);
        loadCustomFunctions();
    }

    /**
     * Loads a single parameter as an R one-element vector of the same name as
     * the parameter's name.
     *
     * @param parameter Single parameter to load.
     */
    private void loadAsRVector(SingleParameter parameter) {
        codeBuffer.append(parameter.getParameterName()).append(" = c(").append(parameter.getParameterValue()).append(")");
    }

    /**
     * Loads a parameter table as one or more R vectors with unique names equal
     * to the parameter's names from the table.
     *
     * @param parameterTable Table of parameters to load.
     */
    private void loadAsRVector(ParameterTable parameterTable) {
        String[] parameterNames = parameterTable.getParameterNames();
        ArrayList<ArrayList<Double>> parameterValues = parameterTable.getParameterValues();
        String prefix = "";
        for (int i = 0; i < parameterNames.length; ++i) {
            StringBuilder values = new StringBuilder(parameterNames[i]);
            values.append(" = c(");
            values.append(parameterValues.get(i).get(0).toString());
            for (int j = 1; j < parameterValues.get(i).size(); ++j) {
                values.append(", ").append(parameterValues.get(i).get(j));
            }
            values.append(")");
            codeBuffer.append(prefix);
            prefix = ", ";
            codeBuffer.append(values);
        }
    }

    /**
     * Load single parameters from HashMap to the R instance.
     *
     * @param parameters TreeMap mapping parameter's names to single parameters.
     */
    private void loadSingleParameters(TreeMap<String, SingleParameter> parameters) {
        if (parameters != null) {
            String prefix = "";
            for (Map.Entry p : parameters.entrySet()) {
                codeBuffer.append(prefix);
                prefix = ", ";
                SingleParameter parameter = (SingleParameter) p.getValue();
                loadAsRVector(parameter);
            }
        }
    }

    /**
     * Load parameter tables from TreeMap to R instance.
     *
     * @param parameters TreeMap mapping table's names to parameter tables.
     */
    private void loadParameterTables(TreeMap<String, ParameterTable> parameters) {
        if (parameters != null) {
            String prefix = "";
            for (Map.Entry p : parameters.entrySet()) {
                codeBuffer.append(prefix);
                prefix = ", ";
                ParameterTable table = (ParameterTable) p.getValue();
                loadAsRVector(table);
            }
        }
    }

    @Override
    public void loadParameters(ParameterSet parameterSet) {
        codeBuffer = new StringBuilder(inputRList + " = list(");
        loadSingleParameters(parameterSet.getSingleParameters());
        loadParameterTables(parameterSet.getParameterTables());
        codeBuffer.append(")");
        code.addRCode(codeBuffer.toString());
        caller.setRCode(code);
        caller.runAndReturnResultOnline(inputRList);
        code.clear();
    }

    @Override
    public double getSingleParameterValue(String parameterName)
            throws NoSuchParameterException, ParseException {
        double[] value = caller.getParser().getAsDoubleArray(parameterName);
        code.clear();
        return value[0];
    }

    @Override
    public ArrayList<Double> getParameterVectorValue(String parameterName)
            throws NoSuchParameterException {
        double[] vector = caller.getParser().getAsDoubleArray(parameterName);
        code.clear();
        ArrayList<Double> parameterVector = new ArrayList<>();
        for (int i = 0; i < vector.length; ++i) {
            parameterVector.add(vector[i]);
        }
        return parameterVector;
    }

    @Override
    public void runCustomCode(String customCode, String resultList) {
        code.addRCode(customCode);
        caller.setRCode(code);
        caller.runAndReturnResultOnline(resultList);
        code.clear();
    }

    @Override
    public void runCustomCode(String customCode) {
        code.addRCode(RInterpreter.helperRList + " = c()");
        code.addRCode("try({" + customCode + "})");
        caller.setRCode(code);
        caller.runAndReturnResultOnline(RInterpreter.helperRList);
        code.clear();
    }

    @Override
    public void redirectOutput() {
        if (!outputRedirected) {
            caller.redirectROutputToConsole();
            outputRedirected = true;
        }
    }

    @Override
    public File startPlot() throws IOException {
        File plotFile = caller.getRCode().startPlot();
        caller.getRCode().addRCode(RInterpreter.helperRList + " = c()");
        caller.runAndReturnResultOnline(RInterpreter.helperRList);
        caller.cleanRCode();
        return plotFile;
    }

    @Override
    public ImageIcon getPlot(File plotFile) {
        return caller.getRCode().getPlot(plotFile);
    }

    @Override
    public void endPlot() {
        caller.getRCode().endPlot();
        caller.getRCode().addRCode(RInterpreter.helperRList + " = c()");
        caller.runAndReturnResultOnline(RInterpreter.helperRList);
        caller.cleanRCode();
    }

    @Override
    public final void loadCustomFunctions() {
        code.clear();
        code.addRCode("help = function(...) {print(\""
                + LanguageProvider.getInstance().getString("rHelpFunctionDisabled")
                + "\")}");
        code.addRCode("q = function(...) {print(\""
                + LanguageProvider.getInstance().getString("rQuitFunctionDisabled")
                + "\")}");
        code.addRCode(
                "normalPlot = function(x, y, ...) {\n"
                + "UseMethod(\"plot\")\n"
                + "}");
        caller.setRCode(code);
        caller.runAndReturnResultOnline(inputRList);
        code.clear();
        code.addRCode(
                "plot = function(x, y, ...) {\n"
                + "newPlotFile = "
                + "tempfile(\"JRplot\", tmpdir = \""
                + tempDirectory
                + "\", \".png\")\n"
                + "file.create(newPlotFile)\n"
                + plotRList + "$generatedPlots <<- append(" + plotRList + "$generatedPlots, "
                + "newPlotFile)\n"
                + "png(tail(" + plotRList + "$generatedPlots, n = 1))\n"
                + "withCallingHandlers(do.call(normalPlot, as.list(match.call()[-1])), error = function(e) {\n"
                + "dev.off()\n"
                + "file.remove(newPlotFile)\n"
                + plotRList + "$generatedPlots <<- "
                + plotRList + "$generatedPlots[-length(" + plotRList + ")]\n"
                + "})\n"
                + "newPlotFile = dev.off()\n"
                + "}");
        caller.setRCode(code);
        caller.runAndReturnResultOnline(plotRList);
        code.clear();
    }

    @Override
    public void cleanUp() {
    }

    @Override
    public String[] getPlotsPaths() {
        String[] paths = new String[0];
        try {
            paths = caller.getParser().getAsStringArray("generatedPlots");
            for (String path : paths) {
                File plotFile = new File(path);
                plotFile.deleteOnExit();
            }
        } catch (ParseException pex) {
            // there are no generated plots
        }
        code.clear();
        code.addRCode(plotRList + " = list(generatedPlots = c())");
        caller.setRCode(code);
        caller.runAndReturnResultOnline(plotRList);
        code.clear();
        return paths;
    }
}
